
package com.lxm.framework.web.http;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.Method;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lxm.framework.common.AppException;
import com.lxm.framework.common.cache.caffeine.CaffeineCache;
import com.lxm.framework.common.utils.CryptoUtils;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

import java.net.HttpCookie;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Author: Lys
 * @Date 2023/1/11
 * @Describe
 **/
@Slf4j
public class HttpClient {

    private final static ObjectMapper objectMapper;
    protected final static int TIMEOUT_MILLIS = 5000;
    // 是否使用缓存
    private final static AtomicBoolean useCache;

    static {
        useCache = new AtomicBoolean(false);
        objectMapper = new ObjectMapper();
    }

    public static <T> T get(@NonNull String url, Map<String, Object> requestParams, @NonNull Class<T> returnClass) {
        return execute(url, HttpMethod.GET, requestParams, null, null, null, null, returnClass, null);
    }

    public static <T> T get(@NonNull String url, Map<String, Object> requestParams, @NonNull TypeReference<T> typeReference) {
        return execute(url, HttpMethod.GET, requestParams, null, null, null, null, null, typeReference);
    }

    public static <T> T post(@NonNull String url, @NonNull Object requestBody, Map<String, Object> requestParams, @NonNull Class<T> returnClass) {
        return execute(url, HttpMethod.POST, requestParams, requestBody, null, null, null, returnClass, null);
    }

    public static <T> T post(@NonNull String url, @NonNull Object requestBody, Map<String, Object> requestParams, @NonNull TypeReference<T> typeReference) {
        return execute(url, HttpMethod.POST, requestParams, requestBody, null, null, null, null, typeReference);
    }

    public static <T> T put(@NonNull String url, Map<String, Object> requestParams, @NonNull Class<T> returnClass) {
        return execute(url, HttpMethod.PUT, requestParams, null, null, null, null, returnClass, null);
    }

    public static <T> T put(@NonNull String url, Map<String, Object> requestParams, @NonNull TypeReference<T> typeReference) {
        return execute(url, HttpMethod.PUT, requestParams, null, null, null, null, null, typeReference);
    }

    public static <T> T delete(@NonNull String url, Map<String, Object> requestParams, @NonNull Class<T> returnClass) {
        return execute(url, HttpMethod.DELETE, requestParams, null, null, null, null, returnClass, null);
    }

    public static <T> T delete(@NonNull String url, Map<String, Object> requestParams, @NonNull TypeReference<T> typeReference) {
        return execute(url, HttpMethod.DELETE, requestParams, null, null, null, null, null, typeReference);
    }

    public static void useCache(boolean use) {
        useCache.set(use);
    }

    /**
     * @param url           url
     * @param httpMethod    method
     * @param returnClass   return-class
     * @param requestParams request params
     * @param requestBody   request body
     * @param headers       headers
     * @param cookies       cookies
     * @param <T>           generic
     * @return target generic
     */
    public static <T> T execute(@NonNull String url, @NonNull HttpMethod httpMethod, Map<String, Object> requestParams, Object requestBody, MediaType mediaType, Map<String, String> headers, Map<String, String> cookies, Class<T> returnClass, TypeReference<T> typeReference) {
        String finalUrl = link(url, requestParams);
        var req = HttpRequest.of(finalUrl)
                .method(toMethod(httpMethod))
                .timeout(TIMEOUT_MILLIS);
        setHeaders(req, headers);
        setCookies(req, cookies);
        setBody(req, requestBody);
        setContentType(req, mediaType);
        String md5 = null;
        if (useCache.get()) {
            md5 = md5(req, requestBody);
            var res = readCache(md5, returnClass);
            if (Objects.nonNull(res)) {
                log.debug("http-client read from cache success");
                return res;
            }
        }
        return mapping(req.execute().body(), returnClass, typeReference, md5);
    }

    /**
     * 请求之后的映射
     *
     * @param requestBodyString 请求结果的string
     * @param returnClass       返回的class
     * @param typeReference     返回的type-reference
     * @param md5               如果有缓存，就有md5的key
     * @param <T>               类型
     * @return T
     */
    private static <T> T mapping(String requestBodyString, Class<T> returnClass, TypeReference<T> typeReference, String md5) {
        try {
            T result;
            if (null != typeReference) {
                final TypeReference<String> StringTypeReference = new TypeReference<>() {
                };
                // 如果要返回的类型是string，就不用转换了，下面同理
                if (StringTypeReference.getType().equals(typeReference.getType())) {
                    return (T) requestBodyString;
                }
                result = objectMapper.readValue(requestBodyString, typeReference);
            } else {
                if (String.class.equals(returnClass)) {
                    return (T) requestBodyString;
                }
                result = objectMapper.readValue(requestBodyString, returnClass);
            }
            if (useCache.get()) {
                putCache(md5, result);
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 若存在header信息
     *
     * @param request 请求
     * @param headers headers
     */
    private static void setHeaders(@NonNull HttpRequest request, Map<String, String> headers) {
        if (Objects.nonNull(headers) && !headers.isEmpty()) {
            headers.forEach(request::header);
        }
    }

    /**
     * 若存在cookie信息
     *
     * @param request 请求
     * @param cookies cookie信息
     */
    private static void setCookies(@NonNull HttpRequest request, Map<String, String> cookies) {
        if (Objects.nonNull(cookies) && !cookies.isEmpty()) {
            var cookieList = new ArrayList<HttpCookie>();
            cookies.forEach((k, v) -> cookieList.add(new HttpCookie(k, v)));
            request.cookie(cookieList);
        }
    }

    /**
     * 若存在body信息
     *
     * @param request 请求
     * @param body    body
     */
    private static void setBody(@NonNull HttpRequest request, Object body) {
        var method = request.getMethod();
        if (method.equals(Method.POST)) {
            if (Objects.nonNull(body)) {
                try {
                    String bodyString = objectMapper.writeValueAsString(body);
                    request.body(bodyString);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 若存在media-type
     *
     * @param request   请求
     * @param mediaType media-type
     */
    private static void setContentType(@NonNull HttpRequest request, MediaType mediaType) {
        if (Objects.isNull(mediaType)) {
            mediaType = MediaType.APPLICATION_JSON;
        }
        request.contentType(mediaType.toString());
    }

    /**
     * 若使用cache
     *
     * @param request     req
     * @param requestBody req-body
     * @return string
     */
    private static String md5(@NonNull HttpRequest request, Object requestBody) {
        String urlHash = String.valueOf(request.getUrl().hashCode());
        String methodHash = String.valueOf(request.getMethod().hashCode());
        String inter = (urlHash).concat(methodHash);
        if (Objects.nonNull(requestBody)) {
            inter = inter.concat(String.valueOf(requestBody.hashCode()));
        }
        return CryptoUtils.md5Hex(inter);
    }

    private static <T> T readCache(String md5, Class<T> returnClass) {
        boolean hasKey = CaffeineCache.hasKey(md5);
        if (hasKey) {
            return CaffeineCache.read(md5, returnClass);
        }
        return null;
    }

    private static void putCache(String md5, Object obj) {
        if (StringUtils.isNotBlank(md5)) {
            CaffeineCache.write(md5, obj);
        }
    }

    private static Method toMethod(HttpMethod httpMethod) {
        switch (httpMethod) {
            case GET -> {
                return Method.GET;
            }
            case POST -> {
                return Method.POST;
            }
            case PUT -> {
                return Method.PUT;
            }
            case DELETE -> {
                return Method.DELETE;
            }
        }
        throw new AppException(-1, "http-client 请求类型只支持GET POST PUT DELETE");
    }

    private static String link(@NonNull String url, Map<String, Object> requestParams) {
        if (Objects.isNull(requestParams) || requestParams.isEmpty()) {
            return url;
        }
        StringBuilder builder = new StringBuilder(url);
        boolean isFirst = true;
        var qmark = url.endsWith("?");
        for (String key : requestParams.keySet()) {
            if (key != null && requestParams.get(key) != null) {
                if (isFirst) {
                    isFirst = false;
                    if (!qmark)
                        builder.append("?");
                } else {
                    builder.append("&");
                }
                var value = String.valueOf(requestParams.get(key));
                if (null == value || StringUtils.isBlank(value))
                    continue;
                builder.append(key)
                        .append("=")
                        .append(value);
            }
        }
        return builder.toString();
    }


}
